 /*******************************************************************************
  * Copyright (c) 2000, 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.update.internal.search;

 import java.util.ArrayList ;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.PluginVersionIdentifier;
 import org.eclipse.core.runtime.Preferences;
 import org.eclipse.update.configuration.IConfiguredSite;
 import org.eclipse.update.configuration.IInstallConfiguration;
 import org.eclipse.update.configuration.ILocalSite;
 import org.eclipse.update.core.IFeature;
 import org.eclipse.update.core.IFeatureReference;
 import org.eclipse.update.core.IIncludedFeatureReference;
 import org.eclipse.update.core.ISite;
 import org.eclipse.update.core.ISiteFeatureReference;
 import org.eclipse.update.core.IURLEntry;
 import org.eclipse.update.core.IUpdateConstants;
 import org.eclipse.update.core.SiteManager;
 import org.eclipse.update.core.VersionedIdentifier;
 import org.eclipse.update.internal.core.Messages;
 import org.eclipse.update.internal.core.UpdateCore;
 import org.eclipse.update.internal.operations.FeatureHierarchyElement;
 import org.eclipse.update.internal.operations.UpdateUtils;
 import org.eclipse.update.operations.IInstallFeatureOperation;
 import org.eclipse.update.operations.OperationsManager;
 import org.eclipse.update.search.IQueryUpdateSiteAdapter;
 import org.eclipse.update.search.IUpdateSearchFilter;
 import org.eclipse.update.search.IUpdateSearchQuery;
 import org.eclipse.update.search.IUpdateSearchResultCollector;

 public class UpdatesSearchCategory extends BaseSearchCategory {
     private static final String CATEGORY_ID =
         "org.eclipse.update.core.new-updates"; //$NON-NLS-1$
 private IFeature [] features;
     private boolean automatic;

     class Candidate {
         ArrayList children;
         Candidate parent;
         IFeatureReference ref;
         public Candidate(IFeatureReference ref) {
             this.ref = ref;
         }
         public Candidate(IFeatureReference ref, Candidate parent) {
             this(ref);
             this.parent = parent;
         }
         public void add(Candidate child) {
             if (children == null)
                 children = new ArrayList ();
             child.setParent(this);
             children.add(child);
         }
         void setParent(Candidate parent) {
             this.parent = parent;
         }
         public IFeatureReference getReference() {
             return ref;
         }
         void setReference(IFeatureReference ref) {
             this.ref = ref;
         }
         public IFeature getFeature(IProgressMonitor monitor) {
             try {
                 return ref.getFeature(monitor);
             } catch (CoreException e) {
                 return null;
             }
         }
         public Candidate getParent() {
             return parent;
         }
         public Candidate getRoot() {
             Candidate root = this;

             while (root.getParent() != null) {
                 root = root.getParent();
             }
             return root;
         }
         public IURLEntry getUpdateEntry() {
             int location = IUpdateConstants.SEARCH_ROOT;

             if (ref instanceof IIncludedFeatureReference)
                 location =
                     ((IIncludedFeatureReference) ref).getSearchLocation();
             if (parent == null || location == IUpdateConstants.SEARCH_SELF) {
                 return getFeature(null).getUpdateSiteEntry();
             }
             return getRoot().getUpdateEntry();
         }
         public String toString() {
             return ref.toString();
         }
         public boolean equals(Object source) {
             if (source instanceof Candidate) {
                 return this.ref.equals(((Candidate) source).getReference());
             }
             if (source instanceof IFeatureReference) {
                 return this.ref.equals(source);
             }
             return false;
         }
         public void addToFlatList(ArrayList list, boolean updatableOnly) {
             // add itself
 if (!updatableOnly || isUpdatable())
                 list.add(this);
             // add children
 if (children != null) {
                 for (int i = 0; i < children.size(); i++) {
                     Candidate child = (Candidate) children.get(i);
                     child.addToFlatList(list, updatableOnly);
                 }
             }
         }
         public boolean isUpdatable() {
             return (parent == null);
         }
     }

     private static class Hit {
         IFeature candidate;
         IFeatureReference ref;
         IInstallFeatureOperation patchFor;
         boolean patch;
         IInstallFeatureOperation job;
         
         public Hit(IFeature candidate, IFeatureReference ref) {
             this.candidate = candidate;
             this.ref = ref;
         }
         public Hit(IFeature candidate, IFeatureReference ref, boolean patch) {
             this(candidate, ref);
             this.patch = patch;
         }

         public Hit(IFeature candidate, IFeatureReference ref, IInstallFeatureOperation patchFor) {
             this(candidate, ref, true);
             this.patchFor = patchFor;
         }

         public IInstallFeatureOperation getJob() {
             if (job == null) {
                 try {
                     IFeature feature = ref.getFeature(null);
                     job = OperationsManager.getOperationFactory().createInstallOperation(null, feature, null, null, null);
                 } catch (CoreException e) {
                     UpdateCore.log(e);
                 }
             }
             return job;
         }

         public boolean isPatch() {
             return patch;
         }
         public IInstallFeatureOperation getPatchedJob() {
             return patchFor;
         }
     }

     public class UpdateQuery implements IUpdateSearchQuery {
         IFeature candidate;
         IQueryUpdateSiteAdapter adapter;

         public UpdateQuery(
             IFeature candidate,
             IURLEntry updateEntry) {
             this.candidate = candidate;
             if (updateEntry != null && updateEntry.getURL() != null)
                 adapter =
                     new QueryUpdateSiteAdapter(
                         getLabelForEntry(updateEntry),
                         updateEntry.getURL(),
                         candidate.getVersionedIdentifier().getIdentifier());
         }
         private String getLabelForEntry(IURLEntry entry) {
             String label = entry.getAnnotation();
             if (label == null || label.length() == 0)
                 label = entry.getURL().toString();
             return label;
         }

         public IQueryUpdateSiteAdapter getQuerySearchSite() {
             return adapter;
         }
         private boolean isBroken() {
             try {
                 IStatus status =
                     SiteManager.getLocalSite().getFeatureStatus(candidate);
                 return status.getSeverity() == IStatus.ERROR;
             } catch (CoreException e) {
                 return false;
             }
         }
         private boolean isMissingOptionalChildren(IFeature feature) {
             try {
                 IIncludedFeatureReference[] children =
                     feature.getIncludedFeatureReferences();
                 for (int i = 0; i < children.length; i++) {
                     IIncludedFeatureReference ref = children[i];
                     try {
                         IFeature child = ref.getFeature(null);
                         // If we are here, the child is not missing.
 // Check it's children recursively.
 if (isMissingOptionalChildren(child))
                             return true;
                     } catch (CoreException e) {
                         // Missing child. Return true if optional,
 // otherwise it is a broken feature that we
 // do not care about.
 if (ref.isOptional()) {
                             return FeatureHierarchyElement.hasOlderVersion(ref);
                         }
                     }
                 }
             } catch (CoreException e) {
             }
             return false;
         }
         public void run(
             ISite site,
             String [] categoriesToSkip,
             IUpdateSearchFilter filter,
             IUpdateSearchResultCollector collector,
             IProgressMonitor monitor) {
             ArrayList hits = new ArrayList ();
             boolean broken = isBroken();
             boolean missingOptionalChildren = false;

             // Don't bother to compute missing optional children
 // if the feature is broken - all we want is to
 // see if we should allow same-version re-install.
 if (!broken)
                 missingOptionalChildren = isMissingOptionalChildren(candidate);
             ISiteFeatureReference[] refs = site.getFeatureReferences();
             monitor.beginTask("", refs.length + 1); //$NON-NLS-1$
 ArrayList updateJobs = new ArrayList ();
             for (int i = 0; i < refs.length; i++) {
                 ISiteFeatureReference ref = refs[i];
                 try {
                     if (isNewerVersion(candidate.getVersionedIdentifier(),ref.getVersionedIdentifier())) {
                         Hit h = new Hit(candidate, ref);
                         hits.add(h);
                         IInstallFeatureOperation job = h.getJob();
                         if (job != null)
                             updateJobs.add(job);
                     } else {
                         // accept the same feature if the installed
 // feature is broken
 if ((broken || missingOptionalChildren)
                             && candidate.getVersionedIdentifier().equals(
                                 ref.getVersionedIdentifier())){
                             hits.add(new Hit(candidate, ref));
                             continue;
                         }
                         else {
                             // check for patches
 if (isPatch(candidate, ref)){
                                 hits.add(new Hit(candidate, ref, true));
                                 continue;
                             }
                         }
                     }
                 } catch (CoreException e) {
                 }
                 monitor.worked(1);
                 if (monitor.isCanceled()){
                     return;
                 }
                 
             }
             // accept patches for updated features
 for (int n = 0; n < updateJobs.size(); n++) {
                 IInstallFeatureOperation job = (IInstallFeatureOperation) updateJobs
                         .get(n);
                     IFeature newCandidate = job.getFeature();
                     for (int i = 0; i < refs.length; i++) {
                         ISiteFeatureReference ref = refs[i];
                         if (isPatch(newCandidate, ref)) {
                             Hit h = new Hit(newCandidate, ref, job);
                             hits.add(h);
                             continue;
                         }
                         //monitor.worked(1);
 if (monitor.isCanceled()) {
                             return;
                         }
                     }
             }
             if (hits.size() > 0) {
                 collectValidHits(hits, filter, collector);
             }
             monitor.worked(1);
             monitor.done();
         }
         
         /**
          * Returns IFeature associated with the IUpdateSearchQuery
          */
         public IFeature getFeature() {
             return candidate;
         }
     }

     private ArrayList candidates;

     public UpdatesSearchCategory() {
         this(true);
     }
     
     public UpdatesSearchCategory(boolean automatic) {
         super(CATEGORY_ID);
         this.automatic = automatic;
     }

     private void collectValidHits(
         ArrayList hits,
         IUpdateSearchFilter filter,
         IUpdateSearchResultCollector collector) {
         //Object[] array = hits.toArray();
 IFeature topHit = null;
         for (int i = 0; i < hits.size(); i++) {
             Hit hit = (Hit) hits.get(i);
             IInstallFeatureOperation job = hit.getJob();
             if (job == null)
                 continue;
             // do not accept updates without a license
 if (!UpdateUtils.hasLicense(job.getFeature())) {
                 UpdateCore.log(job.getFeature().getVersionedIdentifier() + ": " + Messages.DefaultFeatureParser_NoLicenseText, null); //$NON-NLS-1$
 continue;
             }
             IStatus status = null;
             
             // Fix for https://bugs.eclipse.org/bugs/show_bug.cgi?id=132450
 // Only validate for automatic updates because
 // non-automatic once will arrive in the review wizard
 // where additional validation will be performed
 if (automatic) {
                 if( hit.getPatchedJob()==null){
                     status = OperationsManager.getValidator().validatePendingInstall(job.getOldFeature(), job.getFeature());
                 }else{
                     status = OperationsManager.getValidator().validatePendingChanges(new IInstallFeatureOperation[]{hit.getPatchedJob(), job});
                 }
             }
             if (status == null || status.getCode() == IStatus.WARNING) {
                 if (hit.isPatch()) {
                     IFeature patch = job.getFeature();
                     // Do not add the patch if already installed
 IFeature[] sameId = UpdateUtils.getInstalledFeatures(patch, false);
                     if (sameId.length==0) {
                         if (filter.accept(patch))
                             collector.accept(patch);
                     }
                 }
                 else {
                     topHit = job.getFeature();
                     if (filter.accept(topHit))
                         collector.accept(topHit);
                 }
             }
         }
     }

     private void initialize() {
         candidates = new ArrayList ();
         try {
             ILocalSite localSite = SiteManager.getLocalSite();
             IInstallConfiguration config = localSite.getCurrentConfiguration();
             IConfiguredSite[] isites = config.getConfiguredSites();
             for (int i = 0; i < isites.length; i++) {
                 contributeCandidates(isites[i]);
             }
         } catch (CoreException e) {
             UpdateCore.log(
                 Messages.UpdatesSearchCategory_errorSearchingForUpdates,
                 e);
         }
     }

     private void contributeCandidates(IConfiguredSite isite)
         throws CoreException {
         IFeatureReference[] refs = isite.getConfiguredFeatures();
         ArrayList candidatesPerSite = new ArrayList ();
         for (int i = 0; i < refs.length; i++) {
             IFeatureReference ref = refs[i];
             // Don't waste time searching for updates to
 // patches.
 try {
                 if (UpdateUtils.isPatch(ref.getFeature(null)))
                     continue;
             }
             catch (CoreException e) {
                 continue;
             }
             Candidate c = new Candidate(ref);
             candidatesPerSite.add(c);
         }
         // Create a tree from a flat list
 buildHierarchy(candidatesPerSite);
         // Add the remaining root candidates to
 // the global list of candidates.
 candidates.addAll(candidatesPerSite);
     }

     private void buildHierarchy(ArrayList candidates) throws CoreException {
         Candidate[] array =
             (Candidate[]) candidates.toArray(new Candidate[candidates.size()]);
         // filter out included features so that only top-level features remain on the list
 for (int i = 0; i < array.length; i++) {
             Candidate parent = array[i];
             IFeature feature = parent.getFeature(null);
             IFeatureReference[] included =
                 feature.getIncludedFeatureReferences();
             for (int j = 0; j < included.length; j++) {
                 IFeatureReference fref = included[j];
                 Candidate child = findCandidate(candidates, fref);
                 if (child != null) {
                     parent.add(child);
                     child.setReference(fref);
                     candidates.remove(child);
                 }
             }
         }
     }
     private Candidate findCandidate(ArrayList list, IFeatureReference ref) {
         for (int i = 0; i < list.size(); i++) {
             Candidate c = (Candidate) list.get(i);
             if (c.ref.equals(ref))
                 return c;
         }
         return null;
     }

     public IUpdateSearchQuery[] getQueries() {
         initialize();
         ArrayList allCandidates = getAllCandidates();

         IUpdateSearchQuery[] queries =
             new IUpdateSearchQuery[allCandidates.size()];
         for (int i = 0; i < queries.length; i++) {
             Candidate candidate = (Candidate) allCandidates.get(i);
             IFeature feature = candidate.getFeature(null);
             IURLEntry updateEntry = candidate.getUpdateEntry();
             if (feature == null) {
                 queries[i] = null;
             } else {
                 queries[i] = new UpdateQuery(feature, updateEntry);
             }
         }
         return queries;
     }
     
 /**
  * Sets the features for which new updates need to be found. If
  * not set, updates will be searched for all the installed
  * and configured features.
  * @param features the features to search updates for
  */
     public void setFeatures(IFeature [] features) {
         this.features = features;
     }
     
 /**
  * Returns an array of features for which updates need to
  * be found.
  * @return an array of features or <samp>null</samp> if not
  * set.
  */
     public IFeature [] getFeatures() {
         return features;
     }
     /**
      * @param fvi
      * @param cvi
      * @return fvi < cvi
      */
     private boolean isNewerVersion(
         VersionedIdentifier fvi,
         VersionedIdentifier cvi) {
         if (!fvi.getIdentifier().equals(cvi.getIdentifier()))
             return false;
         PluginVersionIdentifier fv = fvi.getVersion();
         PluginVersionIdentifier cv = cvi.getVersion();
         String mode = getUpdateVersionsMode();
         boolean greater = cv.isGreaterThan(fv);
         if (!greater)
             return false;
         if (mode.equals(UpdateCore.EQUIVALENT_VALUE))
             return cv.isEquivalentTo(fv);
         else if (mode.equals(UpdateCore.COMPATIBLE_VALUE))
             return cv.isCompatibleWith(fv);
         else
             return false;
     }

     private boolean isPatch(IFeature candidate, ISiteFeatureReference ref) {
         if (ref.isPatch() == false)
             return false;
         try {
             IFeature feature = ref.getFeature(null);
             if( UpdateUtils.isPatch(candidate, feature))
                 return true;
             // Check if patch is for children
 try {
                 IIncludedFeatureReference[] children =
                     candidate.getIncludedFeatureReferences();
                 for (int i = 0; i < children.length; i++) {
                     IIncludedFeatureReference cref = children[i];
                     try {
                         IFeature child = cref.getFeature(null);
                         if (isPatch(child, ref))
                             return true;
                     } catch (CoreException e) {
                     }
                 }
             } catch (CoreException e) {
             }
             return false;
         } catch (CoreException e) {
             return false;
         }
     }
     
     private String getUpdateVersionsMode() {
         Preferences store = UpdateCore.getPlugin().getPluginPreferences();
         return store.getString(UpdateCore.P_UPDATE_VERSIONS);
     }
 /*
  * This method recursively walks the list of candidates
  * building the flat that starts with the roots but
  * also includes all the children that are updatable
  * (use 'include' clause with a match that is not 'perfect').
  */
     private ArrayList getAllCandidates() {
         ArrayList selected = new ArrayList ();
         for (int i=0; i<candidates.size(); i++) {
             Candidate c = (Candidate)candidates.get(i);
             if (isOnTheList(c))
                 c.addToFlatList(selected, true);
         }
         return selected;
     }
     
     private boolean isOnTheList(Candidate c) {
         if (features==null) return true;
         VersionedIdentifier vid;
         try {
             vid = c.getReference().getVersionedIdentifier();
         }
         catch (CoreException e) {
             return false;
         }
         for (int i=0; i<features.length; i++) {
             IFeature feature = features[i];
             VersionedIdentifier fvid = feature.getVersionedIdentifier();
             if (fvid.equals(vid))
                 return true;
         }
         return false;
     }
 }

